use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct Token {
    pub kind: String,
    pub text: String,
    pub line: usize,
}

#[derive(Debug)]
pub struct Lexer {
    pub src: Vec<char>,
    pub pos: usize,
    pub line: usize,
    pub tokens: Vec<Token>,
    pub errors: Vec<String>,
}

fn keyword_map() -> HashMap<&'static str, &'static str> {
    let mut map = HashMap::new();
    map.insert("const", "CONST");
    map.insert("int", "INT");
    map.insert("void", "VOID");
    map.insert("if", "IF");
    map.insert("else", "ELSE");
    map.insert("while", "WHILE");
    map.insert("break", "BREAK");
    map.insert("continue", "CONTINUE");
    map.insert("return", "RETURN");
    map
}

fn symbol_map() -> HashMap<&'static str, &'static str> {
    let mut map = HashMap::new();
    map.insert("+", "PLUS");
    map.insert("-", "MINUS");
    map.insert("*", "MUL");
    map.insert("/", "DIV");
    map.insert("%", "MOD");
    map.insert("=", "ASSIGN");
    map.insert("==", "EQ");
    map.insert("!=", "NEQ");
    map.insert("<", "LT");
    map.insert(">", "GT");
    map.insert("<=", "LE");
    map.insert(">=", "GE");
    map.insert("!", "NOT");
    map.insert("&&", "AND");
    map.insert("||", "OR");
    map.insert("(", "L_PAREN");
    map.insert(")", "R_PAREN");
    map.insert("{", "L_BRACE");
    map.insert("}", "R_BRACE");
    map.insert("[", "L_BRACKT");
    map.insert("]", "R_BRACKT");
    map.insert(",", "COMMA");
    map.insert(";", "SEMICOLON");
    map
}

impl Lexer {
    pub fn new(src: &str) -> Self {
        Lexer {
            src: src.chars().collect(),
            pos: 0,
            line: 1,
            tokens: Vec::new(),
            errors: Vec::new(),
        }
    }

    fn peek(&self) -> Option<char> {
        self.src.get(self.pos).copied()
    }

    fn bump(&mut self) -> Option<char> {
        let ch = self.peek();
        if let Some(c) = ch {
            self.pos += 1;
            if c == '\n' {
                self.line += 1;
            }
        }
        ch
    }

    pub fn lex(&mut self) {
        let keywords = keyword_map();
        let symbols = symbol_map();

        while let Some(c) = self.peek() {
            if c.is_whitespace() {
                self.bump();
                continue;
            } else if c.is_ascii_alphabetic() || c == '_' {
                self.lex_ident_or_keyword(&keywords);
            } else if c.is_ascii_digit() {
                self.lex_number();
            } else if c == '/' && self.pos + 1 < self.src.len() {
                if self.src[self.pos + 1] == '/' {
                    self.lex_line_comment();
                } else if self.src[self.pos + 1] == '*' {
                    self.lex_block_comment();
                } else {
                    self.lex_operator_or_delim(&symbols);
                }
            } else {
                self.lex_operator_or_delim(&symbols);
            }
        }
    }

    fn lex_ident_or_keyword(&mut self, keywords: &HashMap<&str, &str>) {
        let start = self.pos;
        while let Some(c) = self.peek() {
            if c.is_ascii_alphanumeric() || c == '_' {
                self.bump();
            } else {
                break;
            }
        }
        let text: String = self.src[start..self.pos].iter().collect();
        let kind = keywords
            .get(text.as_str())
            .map(|s| s.to_string())
            .unwrap_or("IDENT".to_string());
        self.tokens.push(Token {
            kind,
            text,
            line: self.line,
        });
    }

    fn lex_number(&mut self) {
        let start = self.pos;
        if self.peek() == Some('0') {
            self.bump();
            if let Some(c) = self.peek() {
                if c == 'x' || c == 'X' {
                    self.bump();
                    while matches!(self.peek(), Some(ch) if ch.is_ascii_hexdigit()) {
                        self.bump();
                    }
                } else {
                    while matches!(self.peek(), Some(ch) if ch.is_ascii_digit()) {
                        self.bump();
                    }
                }
            }
        } else {
            while matches!(self.peek(), Some(ch) if ch.is_ascii_digit()) {
                self.bump();
            }
        }
        let text: String = self.src[start..self.pos].iter().collect();
        // keep textual literal as token text (not converted) to make formatter print original literal
        self.tokens.push(Token {
            kind: "INTEGER_CONST".to_string(),
            text,
            line: self.line,
        });
    }

    fn lex_line_comment(&mut self) {
        // consume "//"
        self.bump();
        self.bump();
        while let Some(c) = self.bump() {
            if c == '\n' {
                break;
            }
        }
    }

    fn lex_block_comment(&mut self) {
        // consume "/*"
        self.bump();
        self.bump();
        while let Some(c) = self.bump() {
            if c == '*' && self.peek() == Some('/') {
                self.bump();
                break;
            }
        }
    }

    fn lex_operator_or_delim(&mut self, symbols: &HashMap<&str, &str>) {
        let start = self.pos;
        let first = self.bump(); // consumes one char
        if first.is_none() {
            return;
        }
        // try two-char symbol
        if let Some(c2) = self.peek() {
            let two: String = self.src[start..=self.pos].iter().collect();
            if symbols.contains_key(two.as_str()) {
                self.bump();
                let kind = symbols.get(two.as_str()).unwrap().to_string();
                self.tokens.push(Token {
                    kind,
                    text: two,
                    line: self.line,
                });
                return;
            }
        }
        let one: String = self.src[start..self.pos].iter().collect();
        if let Some(kind) = symbols.get(one.as_str()) {
            self.tokens.push(Token {
                kind: kind.to_string(),
                text: one,
                line: self.line,
            });
        } else {
            self.errors.push(format!(
                "Error type A at Line {}: Mysterious character \"{}\".",
                self.line, one
            ));
        }
    }
}